/*
 * RED5 Open Source Flash Server - http://code.google.com/p/red5/
 * 
 * Copyright 2006-2014 by respective authors (see below). All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.red5.server;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.StandardMBean;

import org.red5.logging.Red5LoggerFactory;
import org.red5.server.jmx.mxbeans.ContextLoaderMXBean;
import org.red5.server.plugin.PluginRegistry;
import org.slf4j.Logger;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.web.context.support.XmlWebApplicationContext;

/**
 * Red5 applications loader
 * 
 * @author The Red5 Project
 * @author Tiago Jacobs (tiago@imdt.com.br)
 * @author Paul Gregoire (mondain@gmail.com)
 */
@ManagedResource(objectName = "org.red5.server:name=contextLoader,type=ContextLoader", description = "ContextLoader")
public class ContextLoader implements ApplicationContextAware, InitializingBean, DisposableBean, ContextLoaderMXBean {

	protected static Logger log = Red5LoggerFactory.getLogger(ContextLoader.class);

	/**
	 * Spring Application context
	 */
	protected ApplicationContext applicationContext;

	/**
	 * Spring parent app context
	 */
	protected ApplicationContext parentContext;

	/**
	 * Context location files
	 */
	protected String contextsConfig;

	/**
	 * MBean object name used for de/registration purposes.
	 */
	private ObjectName oName;

	/**
	 * Context map
	 */
	protected ConcurrentMap<String, ApplicationContext> contextMap;

	/**
	 * Whether or not a JVM shutdown hook should be added to 
	 * the Spring application context.
	 */
	private boolean useShutdownHook;

	/**
	 * Registers with JMX and registers a shutdown hook.
	 * 
	 * @throws Exception I/O exception, casting exception and others 
	 */
	public void afterPropertiesSet() throws Exception {
		log.info("ContextLoader init");
		// register in jmx
		registerJMX();
		// initialize
		init();
		// check to see if we should add a shutdown hook
		if (useShutdownHook) {
			// register a jvm shutdown hook
			((AbstractApplicationContext) applicationContext).registerShutdownHook();
		}
		LoaderBase jeeServer = applicationContext.getBean(LoaderBase.class);
		// lookup the jee container
		if (jeeServer == null) {
			log.info("JEE server was not found");
		} else {
			log.info("JEE server was found: {}", jeeServer.toString());			
		}
	}

	/**
	 * Un-loads or un-initializes the contexts; this is a shutdown method for this loader.
	 */
	public void destroy() throws Exception {
		log.info("ContextLoader un-init");
		shutdown();
		System.exit(0);
	}

	/**
	 * Loads context settings from ResourceBundle (.properties file)
	 */
	public void init() throws IOException {
		// Load properties bundle
		Properties props = new Properties();
		Resource res = applicationContext.getResource(contextsConfig);
		if (res.exists()) {
			// Load properties file
			props.load(res.getInputStream());
			// Pattern for arbitrary property substitution
			Pattern patt = Pattern.compile("\\$\\{([^\\}]+)\\}");
			Matcher matcher = null;
			// Iterate thru properties keys and replace config attributes with
			// system attributes
			for (Object key : props.keySet()) {
				String name = (String) key;
				String config = props.getProperty(name);
				String configReplaced = config + "";
				//
				matcher = patt.matcher(config);
				//execute the regex
				while (matcher.find()) {
					String sysProp = matcher.group(1);
					String systemPropValue = System.getProperty(sysProp);
					if (systemPropValue == null) {
						systemPropValue = "null";
					}
					configReplaced = configReplaced.replace(String.format("${%s}", sysProp), systemPropValue);
				}
				log.info("Loading: {} = {} => {}", new Object[] { name, config, configReplaced });
				matcher.reset();
				// Load context
				loadContext(name, configReplaced);
			}
			patt = null;
			matcher = null;
		} else {
			log.error("Contexts config must be set");
		}
	}

	/**
	 * Loads a context (Red5 application) and stores it in a context map, then adds
	 * it's beans to parent (that is, Red5)
	 * 
	 * @param name Context name
	 * @param config Filename
	 */
	public void loadContext(String name, String config) {
		log.debug("Load context - name: {} config: {}", name, config);
		//check the existence of the config file
		try {
			File configFile = new File(config);
			if (!configFile.exists()) {
				log.warn("Config file was not found at: {}", configFile.getCanonicalPath());
				configFile = new File("file://" + config);
				if (!configFile.exists()) {
					log.warn("Config file was not found at either: {}", configFile.getCanonicalPath());
				} else {
					config = "file://" + config;
				}
			}
		} catch (IOException e) {
			log.error("Error looking for config file", e);
		}
		// add the context to the parent, this will be red5.xml
		ConfigurableBeanFactory factory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
		if (factory.containsSingleton(name)) {
			log.warn("Singleton {} already exists, try unload first", name);
			return;
		}
		// if parent context was not set then lookup red5.common
		if (parentContext == null) {
			log.debug("Lookup common - bean:{} local:{} singleton:{}",
					new Object[] { factory.containsBean("red5.common"), factory.containsLocalBean("red5.common"), factory.containsSingleton("red5.common"), });
			parentContext = (ApplicationContext) factory.getBean("red5.common");
		}
		if (config.startsWith("/")) {
			// Spring always interprets files as relative, so will strip a leading slash unless we tell
			// it otherwise. It also appears to not need this for Windows
			// absolute paths (e.g. C:\Foo\Bar) so we don't catch that either
			String newConfig = "file://" + config;
			log.debug("Resetting {} to {}", config, newConfig);
			config = newConfig;
		}
		ApplicationContext context = new FileSystemXmlApplicationContext(new String[] { config }, parentContext);
		log.debug("Adding to context map - name: {} context: {}", name, context);
		if (contextMap == null) {
			contextMap = new ConcurrentHashMap<String, ApplicationContext>(3, 0.9f, 1);
		}
		contextMap.put(name, context);
		// Register context in parent bean factory
		log.debug("Registering - name: {}", name);
		factory.registerSingleton(name, context);
	}

	/**
	 * Unloads a context (Red5 application) and removes it from the context map, then removes
	 * it's beans from the parent (that is, Red5)
	 * 
	 * @param name Context name
	 */
	public void unloadContext(String name) {
		log.debug("Un-load context - name: {}", name);
		ApplicationContext context = contextMap.remove(name);
		log.debug("Context from map: {}", context);
		String[] bnames = BeanFactoryUtils.beanNamesIncludingAncestors(context);
		for (String bname : bnames) {
			log.debug("Bean: {}", bname);
		}
		ConfigurableBeanFactory factory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory();
		if (factory.containsSingleton(name)) {
			log.debug("Context found in parent, destroying: {}", name);
			FileSystemXmlApplicationContext ctx = (FileSystemXmlApplicationContext) factory.getSingleton(name);
			if (ctx.isRunning()) {
				log.debug("Context was running, attempting to stop");
				ctx.stop();
			}
			if (ctx.isActive()) {
				log.debug("Context is active, attempting to close");
				ctx.close();
			} else {
				try {
					factory.destroyBean(name, ctx);
				} catch (Exception e) {
					log.warn("Context destroy failed for: {}", name, e);
					ctx.destroy();
				} finally {
					if (factory.containsSingleton(name)) {
						log.debug("Singleton still exists, trying another destroy method");
						((DefaultListableBeanFactory) factory).destroySingleton(name);
					}
				}
			}
		} else {
			log.debug("Context does not contain singleton: {}", name);
		}
		context = null;
	}

	/**
	 * Shut server down.
	 */
	public void shutdown() {
		log.info("Shutting down server");
		unregisterJMX();
		//shutdown the plug-in launcher here
		try {
			PluginRegistry.shutdown();
		} catch (Exception e) {
			log.warn("Exception shutting down plugin registry", e);
		}
		if (contextMap != null) {
			log.debug("Context map: {}", contextMap);
			try {
				//unload all the contexts in the map
				for (Map.Entry<String, ApplicationContext> entry : contextMap.entrySet()) {
					String contextName = entry.getKey();
					log.debug("Unloading context {} on uninit", contextName);
					unloadContext(contextName);
				}
				contextMap.clear();
			} catch (Exception e) {
				log.warn("Exception shutting down contexts", e);
			}
		}
	}

	/**
	 * Return context by name
	 * 
	 * @param name Context name
	 * @return Application context for given name
	 */
	public ApplicationContext getContext(String name) {
		if (contextMap != null) {
			return contextMap.get(name);
		} else {
			return null;
		}
	}

	/**
	 * Sets a parent context for child context based on a given key.
	 * 
	 * @param parentContextKey key for the parent context
	 * @param appContextId id of the child context
	 */
	public void setParentContext(String parentContextKey, String appContextId) {
		log.debug("Set parent context {} on {}", parentContextKey, appContextId);
		ApplicationContext parentContext = getContext(parentContextKey);
		if (parentContext != null) {
			XmlWebApplicationContext childContext = (XmlWebApplicationContext) getContext(appContextId);
			if (childContext != null) {
				childContext.setParent(parentContext);
			} else {
				log.debug("Child context not found");
			}
		} else {
			log.debug("Parent context not found");
		}
	}

	protected void registerJMX() {
		// register with jmx
		MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
		try {
			oName = new ObjectName("org.red5.server:name=contextLoader,type=ContextLoader");
			// check for existing registration before registering
			if (!mbs.isRegistered(oName)) {
				mbs.registerMBean(new StandardMBean(this, ContextLoaderMXBean.class, true), oName);
			} else {
				log.debug("ContextLoader is already registered in JMX");
			}
		} catch (Exception e) {
			log.warn("Error on jmx registration", e);
		}
	}

	protected void unregisterJMX() {
		MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
		try {
			mbs.unregisterMBean(oName);
		} catch (Exception e) {
			log.warn("Exception unregistering: {}", oName, e);
		}
		oName = null;
	}

	/**
	 * @param applicationContext Spring application context
	 * @throws BeansException Top level exception for app context (that is, in fact, beans
	 *             factory)
	 */
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	/**
	 * Setter for parent application context
	 * 
	 * @param parentContext Parent Spring application context
	 */
	public void setParentContext(ApplicationContext parentContext) {
		this.parentContext = parentContext;
	}

	/**
	 * Return parent context
	 * 
	 * @return parent application context
	 */
	public ApplicationContext getParentContext() {
		return parentContext;
	}

	/**
	 * Setter for context config name
	 * 
	 * @param contextsConfig Context config name
	 */
	public void setContextsConfig(String contextsConfig) {
		this.contextsConfig = contextsConfig;
	}

	public String getContextsConfig() {
		return contextsConfig;
	}

	/**
	 * Whether or not the shutdown hook is enabled.
	 * 
	 * @return true if enabled, false otherwise
	 */
	public boolean isUseShutdownHook() {
		return useShutdownHook;
	}

	/**
	 * Enables or disables the shutdown hook.
	 * 
	 * @param useShutdownHook true to enable, false to disable
	 */
	public void setUseShutdownHook(boolean useShutdownHook) {
		this.useShutdownHook = useShutdownHook;
	}

}
